【Vue.js源码学习】--响应式原理

本文主要深入分析 vue.js 2.6.12 版本的源码,了解底层的实现,学习 Vue.js 处理问题的方式,包括 Vue.js 初始化开始、首次渲染的过程、响应式的依赖收集、数据响应式原理、Watcher 渲染视图等。

vue 相关准备

Vue 源码地址

vue 源码目录结构

├── scripts ------------------ 打包相关的配置文件,其中最重要的是config.js主要根据不同的入口,打包为不同的文件
├── dist --------------------- 打包之后文件所在位置
├── examples ----------------- demo示例
├── flow --------------------- Vue使用了Flow来进行静态类型检查,这里定义了声明了一些态类型
├── packages ----------------- vue还可以分别生成其它的npm包
├── src ---------------------- 主要源码所在位置
    ├── compiler ------------- 编译相关(vue中把模板转换成render函数)
        ├── codegen ---------- 根据ast生成render函数
        ├── directives ------- 通用生成render函数之前需要处理的指令
        ├── parser ----------- 模板解析
    ├── core ----------------- 核心代码
        ├── components ------- 全局的组件,这里只有keep-alive
        ├── global-api ------- 全局方法,也就是添加在Vue对象上的方法,比如Vue use, Vue.extend,Vue.mixi等
        ├── instance --------- 实例相关内容,包括实例方法,生命周期,事件等
        ├── observer --------- 双向数据绑定相关文件
        ├── util ------------- 工具方法
        ├── vdom ------------- 虚拟dom相关 重写了snabbdom,增加了组件机制
    ├── platforms ------------ 不同平台的支持
        ├── web -------------- web端独有文件
            ├── compiler ----- 编译阶段需要处理的指令和模块
            ├── runtime ------ 运行阶段需要处理的组件、指令和模块
            ├── server ------- 服务端渲染相关
            ├── util --------- 工具库
        ├── weex ------------- weex端独有文件
    ├── server --------------- 服务端渲染
    ├── sfc ------------------ vue 文件解析
    ├── shared --------------- 共享工具代码
├── test --------------------- 测试用例

我们可以看到,Vue 在开发的时候首先会按照功能把代码拆分到不同的文件夹,然后再拆分成小的模块,这样的代码结构清楚,可以提高其可读性和可维护性。

Flow

  • Flow 是 JavaScript 的静态类型检查器(在编译前做检查,类似于 C# 和 Java )
  • Flow 的静态类型检查错误是通过静态类型推断实现的,文件开头通过// @flow或者/* @flow */声明

    1
    2
    3
    4
    5
    /* @flow */ 
    function square(n: number): number {
    return n * n;
    }
    square("2"); // Error!

    vue2.x 使用 Flow 来检测,而最新版本 vue3.0 使用 TypeScript ,TypeScript 和 Flow 一样都是 JavaScript 静态类型检查器,最终都会编译成 JavaScript,所以此处只针对 Flow 做了解即可。

调试设置

  • 打包工具 Rollup

    • Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
    • Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
    • Rollup 打包不会生成冗余的代码,webpack 会生成一些浏览器支持的模块化代码
  • 设置 sourcemap
    package.json 文件中的 dev 脚本中添加参数 –sourcemap,记录源码和打包代码对应关系,方便调试

    1
    2
    3
    4
    5
      {
    "scripts": {
    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"
    }
    }
    • -w:watch 监视源码的变化,当源码发生变化时,立即重新打包;
    • -c:设置配置文件
    • --sourcemap:开启代码地图,在调试时,可以直接进入 src 中查看源码
    • --environment:设置环境变量,通过设置的环境变量,打包不同版本的 Vue

    通过npm run dev执行打包,生成完整版的vue(dist/vue.js)

  • 调试
    examples 的示例中引入的 vue.min.js 改为 vue.js ,打开 Chrome 的调试工具中的 source

    source

Vue 的不同构建版本

官方文档 - 对不同构建版本的解释

UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
Full (production) vue.min.js
Runtime-only (production) vue.runtime.min.js
  • 完整版:同时包含编译器运行时的版本。
  • 编译器:用来将模板字符串(template)编译成为 JavaScript 渲染函数(render –> vnode)的代码,体积大、效率低。
  • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。
  • UMD:UMD 版本通用的模块版本,支持多种模块方式,以通过 <script> 标签引入, vue.js 默认文件就是运行时 + 编译器的UMD 版本。
  • CommonJS(cjs):CommonJS 版本用来配合老的打包工具比如Browserifywebpack 1
  • ES Module:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件:
    • 为打包工具提供的 ESM:为诸如 webpack 2Rollup 提供的现代打包工具。ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
    • 为浏览器提供的ESM(2.6+):用于在现代浏览器中通过<script type="module">直接导入

Runtime + Compiler(完整版) vs. Runtime-only(运行时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Runtime + Compiler 
// 完整版会将模板转换为render函数,需要编译器
const vm = new Vue({
el: '#app',
template: '<h1>{{ msg }}</h1>',
data: {
msg: 'Hello Vue'
}
})
// compiler
// 运行时不会编译模板,不需要使用编译器
const vm = new Vue({
el: '#app',
render(h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})

项目中推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%,基于 Vue-CLI 创建的项目默认使用基于 ESM 的方式的运行时 vue 版本,即 vue.runtime.esm.js

使用 Vue-CLI 创建的项目并不能直接看到 vue 的构建版本,可以使用 Vue-CLI 提供的工具查看 webpack 的配置文件(Vue-CLI 对 webpack 做了深度的封装,在项目中看不到)

1
$ vue inspect > output.js #将获取到的webpack配置文件输出到output.js 文件中

DOM

单文件组件(*.vue)在运行时候是不需要编译器的,浏览器不支持这些单文件组件打包的时候会将这些组件转换成js对象,并将模板转换为render函数所以单文件组件在运行时是不需要编译器的

源码解析

入口开始

寻找入口文件

以dist/vue.js 的构建过程为例

  • 执行构建

    1
    2
    3
    npm run dev 
    # "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
    # --environment TARGET:web-full-dev 设置环境变量 TARGET
  • scripts/config.js 的执行过程

    • 作用:生成 rollup 构建的配置文件
    • 使用环境变量 TARGET = web-full-dev
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 判断环境变量是否有 TARGET,如果有的话使用genConfig()生成rollup配置文件
    // 获取环境变量TARGET (package.json/scripts中设置)
    if (process.env.TARGET) {
    module.exports = genConfig(process.env.TARGET)
    } else {
    // 否则获取全部配置
    exports.getBuild = genConfig
    exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
    }
  • genConfig(name)

    • 根据环境变量 TARGET 获取配置信息
    • builds[name] 获取生成配置的信息
1
2
3
4
5
6
7
8
9
10
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
// 头文件 /* @flow */
banner
},
  • resolve()

    • 获取入口和出口文件的绝对路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const aliases = require('./alias')
    // 将传入的路径转换为绝对路径
    const resolve = p => {
    // 根据路径中的前半部分去 alias 模块中找别名对应的路径
    const base = p.split('/')[0] // web / dist
    if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
    } else {
    return path.resolve(__dirname, '../', p)
    }
    }
  • alias 模块

定义别名,简化路径书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将传入的参数 转化为 绝对路径
// __dirname 当前文件所在的绝对路径
const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}

整个构建过程是把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 --sourcemap 会生成 vue.js.map,入口文件即为entry-runtime-with-compiler.js

从入口文件开始分析vue的源码

vue源码模块比较多,我们带着问题来查看源码,方便我们阅读源码,下面通过查看入口文件来解决下面的问题

如果同时设置template和render此时会渲染什么?

1
2
3
4
5
6
7
const vm = new Vue({
el: '#app',
template: '<h1>Hello Template</h1>',
render(h) {
return h('h1', 'Hello Render')
}
})

入口文件代码:src/platforms/web/entry-runtime-with-compiler.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 保留vue实例的$mount方法(把生成的BOM挂载到页面上)
const mount = Vue.prototype.$mount
// 重写 ./runtime/index 文件中的 $mount
// $mount 将生成的代码挂载到页面中
Vue.prototype.$mount = function (
// el: 创建 vue 实例时,传入的选项
el?: string | Element,
// 非ssr情况下为false,ssr时候为true
hydrating?: boolean
): Component {
// 获取el对象,即DOM对象
el = el && query(el)

/* istanbul ignore if */
// el不能是body或者html
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
// 创建vue实例时,传入的选项
const options = this.$options
// resolve template/el and convert to render function
// 把template/el转换成render函数
if (!options.render) {
let template = options.template
// 如果模板存在
if (template) {
if (typeof template === 'string') {
// 如果模板是id选择器
if (template.charAt(0) === '#') {
// 获取对应的DOM对象innerHTML
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
// 否则返回当前实例
return this
}
} else if (el) {
// 如果没有template,获取el的outerHTML作为模板
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用mount方法,渲染DOM
return mount.call(this, el, hydrating)
}

通过源码可以知道

  • el 不能是 body 或者 html 标签
  • 如果没有 render,会把 templat e转换成 render 函数
  • 如果有 render 方法直接调用 mount 挂载 DOM

这里有个问题:$mount在什么时候调用?

最简单的方式就是通过调试代码查看call Stack(调用堆栈)清晰看到方法调用过程

注意:如果你最后执行了 npm run build 操作,dist/vue.js 中的最后一行的 sourceMap 映射 //# sourceMappingURL=vue.js.map 会被清除,所以如果想在调试过程看到 src 源码,需要重新 npm run dev 开启代码地图。

$mount

$mount

从而我们得知:$mount_init() 调用的,同时也验证了开始的答案:如果 new Vue 同时设置了 templaterender() ,此时只会执行 render()

Vue 的构造函数在哪? Vue 实例的成员 / Vue 的静态成员 从哪里来的?

Vue 的初始化

四个导出 Vue 的模块

  • src/platforms/web/entry-runtime-with-compiler.js 完整版
    • web 平台相关的入口,重点实现编译
    • 重写了平台相关的 $mount() 方法,将 template 转换成 render 函数
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-modelv-show
    • 注册和平台相关的全局组件: v-transitionv-transition-group
    • 全局方法:
      • __patch__:把虚拟 DOM 转换成真实 DOM
      • $mount:挂载方法,将 DOM 渲染到页面中
  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • Vue 中混入了常用的实例成员

初始化 Vue 的静态方法

可参考 Vue 全局 API 文档
src/core/index.js中注册了vue的静态方法initGlobalAPI,在src/core/global-api/index.js中初始化vue的静态方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
// 初始化 Vue.config 对象
// 在 Vue 中 定义 config 属性
Object.defineProperty(Vue, 'config', configDef)
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 静态方法 set/delete/nextTick,直接挂在到vue的构造函数上
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

// 2.6 explicit observable API
// 让一个对象可响应,设置响应式数据
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}

// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters
Vue.options = Object.create(null) // 原型等于 null,即不需要原型,提高性能
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})

Vue.options._base = Vue
// 设置 keep-alive 组件
// Vue.options.components 注册全局组件
extend(Vue.options.components, builtInComponents)

// 注册 Vue.use() 用来注册插件
initUse(Vue)
// 注册 Vue.mixin() 实现混入
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
initExtend(Vue)
// 注册 Vue.directive()、Vue.component()、Vue.filter()
initAssetRegisters(Vue)
}

初始化 vue的实例成员

可参考 Vue 实例 文档
通过刚才调试 $mount 调用我们可以看到 vue 的构造函数和初始化 vue 的实例成员是在src/core/instance/index.js中定义,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 创建 Vue 构造函数
// 此处不用 class 的原因,是因为方便后续给Vue 实例混入实例成员
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue) // 判断this是否指向vue实例
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 调用_init方法
this._init(options)
}
// 2. 注册 Vue 实例成员
// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 初始化 vm 的属性 $data/$props
// 注册 vm 的方法:$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update()/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
实例成员 - init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
export function initMixin (Vue: Class<Component>) {
// 给vue实例增加_init()方法
// 合并options / 初始化操作
// 整个vue的入口
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid 唯一标识
vm._uid = uid++
// 开发环境下的性能检测
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to avoid this being observed
// 如果是vue实例,则不需要被observe
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// vm 的事件监听初始化,父组件绑定在当前组件上的事件
// $children/$parent/$root/$refs
initLifecycle(vm)
// vm 的事件监听初始化,父组件绑定在当前组件上的事件
initEvents(vm)
// vm 的编译 render 初始化
// $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// beforeCreate 生命钩子的回调
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上,实现依赖注入
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
// 初始化 provide,实现依赖注入
initProvide(vm) // resolve provide after data/props
// created 生命钩子的回调
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
// 调用$mount() 挂载整个页面
vm.$mount(vm.$options.el)
}
}

}
实例成员 - initState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { observe } from '../observer/index'
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 把props中的成员转换成响应式数据,并且注入到vue实例中
if (opts.props) initProps(vm, opts.props)
// 把opts.methods注入到vue实例中
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 有data属性时候调用initData(),把Data中的成员注入到vue实例,并转换为响应式的对象
initData(vm)
} else {
// 没有data属性时候vm添加一个空对象,赋值为true并设置为响应式的
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed/watch并注入到vue实例中
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

首次渲染过程

  • Vue 初始化完毕,开始真正的执行
  • 调用 new Vue() 之前,已经初始化完毕
  • 通过调试代码,记录首次渲染过程

首次渲染过程

1.在 src/core/index.js 中调用 initGlobalAPI(Vue) ,初始化 Vue 静态成员

initGlobalAPI(Vue) 在 src/core/global-api/index.js 中定义

2.在 src/core/instance/index.js 中,定义 Vue 的构造函数
3.在 src/core/instance/index.js ,调用多个注册 Vue 实例成员的方法,实现 Vue 的初始化
4.执行 new Vue() 时,会找到 src/core/instance/index.js 文件中的 vue 构造函数,并创建 Vue 的实例,调用 init() 方法
5._init() 是在 src/core/instance/init.js 文件中定义的的 initMixin() 中注册的,初始化 vm,并且调用 vm.$mount() 挂载整个页面
6.首先,会找到 src/platforms/web/entry-runtime-with-compiler.js 中定义的 mount() 方法 ,根据用户传入的 this.options ,判断是否传入了 render 函数,若没有,则调用 compileToFunctions()template 转化为 render 函数,并将 render 函数存入 options.render 中。最后会返回 mount.call(this, el, hydrating),调用 mount()
7.然后,会执行 src/platforms/web/runtime/index.js 中定义的 $mount(),并返回 mountComponent(this, el, hydrating),运行时版本不会执行这个入口
8.mountComponent()src/core/instance/lifecycle.js 中定义,
1)会先判断用户是否传入 render 函数,如果没有传入 render 函数,会创建空 VNode;并且如果当前是开发环境的话,会发送警告
2)会调用 callHook(),触发 beforeMount 生命钩子;
3)定义 updateComponent (更新组件),实现挂载,会调用 vm._update(vm._render(), hydrating)vm._update()src/core/instance/lifecycle.js 中定义,将 VNode 转换为真实 DOMvm._rendersrc/core/instance/render.js 中定义,渲染虚拟 DOM
4)创建 Watcher 实例,并且传递 updateComponent ,调用 get() 方法
9.在 src/core/observer/watcher.js 中定义 Watcher 类,
1)创建完 watcher 会调用一次 get()
2)调用 updateComponent()
3)调用 vm._render() ,创建 VNode
4)调用 vm.update(vnode, …)
10.在 mountComponent() 的最后,会触发 mounted 生命钩子,此时页面渲染完成;并返回 vm (Vue 实例)。

数据响应式原理

数据响应式和双向绑定机制是使用数据开发驱动的基石,数据响应式:数据发生改变时候自动更新视图,不需要手动更新DOM。

在数据响应式中我们可能会遇到下面这些问题,通过查看源码来回答这些问题:

  • vm.msg = { count: 0 } 重新给属性赋值,是否是响应式的?
  • vm.arr.push(4) 视图是否会更新?
  • vm.arr[0] = 4 给数组元素赋值,视图是否会更新?
  • vm.arr.length = 0 修改数组的 length,视图是否会更新?

响应式处理的入口

整个响应式处理的过程是比较复杂的,我们先找到入口,在循循渐进方式去查看内部的原理

在上面实例成员 - initState这节中我们可以在 initState() 中使用到了 observe(vm._data = {}, true /* asRootData */) ,当没有 data 属性时候的 vm._data 设置为空对象,赋值为 true 设置为响应式的,通过这个方法找到了入口文件,即在 src/core/observer/index.js 中创建 observe 方法,这个方法的作用是:负责为每一个 Object 类型的 value 创建一个 observer 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
// 试图为一个value创建一个observer观察者实例,
// 如果成功观察到,则返回新的观察者,
// 如果该值已经有观察者,则返回现有的观察者
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断 value 是否是对象 是否是 VNode虚拟DOM 的实例
// 如果不是对象/是VNode实例:不需要做响应式处理 直接返回
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果value有__ob__(Observer对象)属性 && value.__ob__是否是 Observer 的实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 赋值ob并直接返回
ob = value.__ob__
}
/**
* isPlainObject(value)):value是否是一个纯粹的JavaScript对象
* !value._isVue:value是否是vue实例<创建vue实例的时候初始化_isVue=true>
*/
else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建一个observer对象并把value转换为getter和setter
ob = new Observer(value)
}
// 如果处理的是跟数据
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates. (收集依赖和派发更新<发送通知>)
*/
// 观察者类,附加到每个被观察对象上
// 一旦被附加,观察者就会将目标对象的属性键转换为getter/setter,
// 以收集依赖关系并派发更新
export class Observer {
// 观察对象
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例的VMCount为0
this.vmCount = 0
// 将当前 observer 实例 挂载到 观察对象的 __ob__ 属性
// def(), 对 Object.defineProperty() 的封装
// this 指向 Observer 的实例 ob
// vm._data.__ob__ = value.__ob__ = this = new Observer(value)
//'__ob__'作用:记录Observer 对象
def(value, '__ob__', this)
// 数组的响应式处理(核心:重写可以改变数组元素的原生方法)
if (Array.isArray(value)) {
/* 下面代码作用:重写会改变数组中元素的方法,当这些方法被调用时候会调用dep.notify
当数组中元素发生变化的时候要通知watcher更新视图 */
// 判断当前浏览器是否支持__proto__(对象原型),也就是用来处理浏览器兼容性问题
if (hasProto) {
// 改变当前数组对象的原型属性,当前数组的原型指向arrayMethods
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历数组中所以元素,把对象属元素转换为响应式对象
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成setter/getter
this.walk(value)
}
}

/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
// 获取观察对象的每一个属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
// 把属性转换成setter/getter,收集依赖,发送通知
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
// 对数组做响应式处理
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
walk(obj)

遍历 obj 的所有属性,为每一个属性调用 defifineReactive() 方法,设置 getter/setter

对象响应式处理 defifineReactive
1
defifineReactive(obj, key, val, customSetter, shallow)'
  • 为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
  • 如果该属性的值是对象,继续调用 observe
  • 如果给属性赋新值,继续调用 observe
  • 如果数据更新发送通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* Define a reactive property on an Object.(为一个对象定义一个响应式的属性)
*/
export function defineReactive(
// 目标对象
obj: Object,
// 转换的属性
key: string,
// 转换的属性的属性值
val: any,
// 用户自定义的 setter 函数(很少用)
customSetter?: ?Function,
// true,只监听对象的第一层属性;
// false,深度监听,即当内部的属性为对象时,深度监听属性内部的属性
shallow?: boolean
) {
// 创建依赖对象实例
// 负责为当前属性 key 收集依赖,即收集当前观察属性的 Watcher
const dep = new Dep()

// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
// property.configurable === false 当前属性不可配置
// 即 不可以通过 delete 删除,
// 并且不可以通过 Object.defineProperty() 进行重新设置
if (property && property.configurable === false) {
return
}

// 提供预定义的存取器函数
// cater for pre-defined getter/setters
// 将用户传入obj中的getter/setters取出来,重写getter/setters,增加依赖收集和通知功能
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
// shallow:false表示不是浅层的监听
// val如果是对象,通过observe监听对象的所有属性,即把对象中的属性转换为getter/setters,也就是深度监听
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
get: function reactiveGetter() {
// 如果预定义(用户设置)的 getter 存在则 value 等于 getter 调用的返回值,否则直接赋予属性值
// 简单来说:如果用户设置了getter,会通过用户设置的getter获取属性值,否则获取刚返回的值val
const value = getter ? getter.call(obj) : val
// 依赖收集:依赖该属性的watcher 对象,添加到Dep的sub数组中,将来数据发生变化的时候,通知所有的watcher
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖(target中存储的是 watcher 对象)
/*Dep.target什么时候初始化的?
在lifecycle.js中的mountComponent中创建了Watcher对象
在Watcher中的get方法的pushTarget中给Dep.target赋值
*/
if (Dep.target) {
// 属性收集依赖 (当前的watcher对象,添加到Dep的sub数组中)
// 例如数组重新复制
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系
if (childOb) {
// 数组对象收集依赖(例如数组元素发生变化时候) 给子对象添加依赖,当子对象改变时候发送通知
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter(newVal) {
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 如果新值等于旧值 或者 新值旧值为 NaN,则不执行
// (newVal !== newVal && value !== value):判断NAN情况,NAN不等于他自身
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 如果没有 setter 直接返回,此处的属性是只读的
if (getter && !setter) return
// 如果预定义 setter 存在则调用,否则直接更新新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是对象,观察子对象并返回子的 observer 对象
childOb = !shallow && observe(newVal)
// 派发更新(发布更改通知)
dep.notify()
}
})
}
数组的响应式处理
  • Observer 的构造函数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 获取 arrayMethods 特有的成员 返回的是包含名字的数组
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export class Observer {
...
constructor (value: any) {
...
if (Array.isArray(value)) {
/* 下面代码作用:重写会改变数组中元素的方法,当这些方法被调用时候会调用dep.notify
当数组中元素发生变化的时候要通知watcher更新视图 */
// 判断当前浏览器是否支持__proto__(对象原型),也就是用来处理浏览器兼容性问题
if (hasProto) {
// 改变当前数组对象的原型属性,当前数组的原型指向arrayMethods
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历数组中所以元素,把对象属元素转换为响应式对象
this.observeArray(value)
} else {
...
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
// 通过使用__proto__拦截原型链来增强目标对象或数组
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
// 通过定义隐藏属性来增强目标对象或数组
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
}
  • 处理数组修改数据的方法 arrayMethods

    src/core/observer/array.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    import { def } from '../util/index'

    const arrayProto = Array.prototype
    // 使用数组的原型创建一个新对象,对象原型指向了数组的prototype
    export const arrayMethods = Object.create(arrayProto)
    // 修改数组元素的方法,这些方法都会修改数组的原数组
    // 数组原生方法不知道dep存在,更不会调用dep.notify()
    const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
    ]

    /**
    * Intercept mutating methods and emit events
    */
    methodsToPatch.forEach(function (method) {
    // cache original method
    // 保存数组原方法
    const original = arrayProto[method]
    // 调用Object.defineProperty()重新定义修改数组的方法
    def(arrayMethods, method, function mutator (...args) {
    // 执行数组的原始方法
    const result = original.apply(this, args)
    // 获取数组对象的ob对象
    const ob = this.__ob__
    let inserted
    switch (method) {
    case 'push':
    case 'unshift':
    inserted = args
    break
    case 'splice':
    // splice第三个元素是新增的元素
    inserted = args.slice(2)
    break
    }
    // 对插入的新元素,重新遍历数组元素设置为响应式数据
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 调用修改了数组的方法,调用数组的ob对象发送通知
    ob.dep.notify()
    return result
    })
    })
dep 类
  • 依赖对象
  • 记录 watcher 对象
  • depend() – watcher 记录对应的 dep
  • 发布通知

src/core/observer/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export function defineReactive (...) {
// 创建依赖对象实例 收集每一个属性的依赖
const dep = new Dep()
...
// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
...
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
if (Dep.target) {
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系
if (childOb) {
// 为当前子对象收集依赖
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter (newVal) {
...
// 派发更新(发布更改通知)
dep.notify()
}
})
}

src/core/observer/dep.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
// dep 是个可观察对象,可以有多个指令订阅它
export default class Dep {
// 静态属性,watcher 对象
static target: ?Watcher;
// dep 实例 Id
id: number;
// dep 实例对应的 watcher 对象/订阅者数组
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

// 添加新的订阅者 watcher 对象
addSub (sub: Watcher) {
this.subs.push(sub)
}

// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}

// 将观察对象和 watcher 建立依赖
depend () {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 数据更新时候会调用notify方法
// 发布通知
notify () {
// stabilize the subscriber list first
// 克隆subs数组
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 按照watcher的创建顺序进行排序
subs.sort((a, b) => a.id - b.id)
}
// 调用每个订阅者的update方法实现更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次也只能有一个 watcher 被使用
Dep.target = null // 存储当前正在执行的目标对象
const targetStack = []

/* 入栈并将当前 watcher 赋值给 Dep.target
父子组件嵌套的时候先把父组件对应的watcher入栈,
载入处理子组件的watcher,子组件处理完后,再把父组件对应的watcher出栈,继续操作
*/
export function pushTarget (target: ?Watcher) {
// 每一个组件都有一个watcher,组件中存在嵌套时,需要存储父组件中的 watcher
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
// 当子组件渲染完之后,会把对应的watcher从栈中弹出,继续执行父组件渲染
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

  1. defineReactive()getter 中创建 dep 对象,并判断 Dep.target 是否有值,如果有, 调用 dep.depend()
  2. dep.depend() 内部调用 Dep.target.addDep(this),也就是 watcheraddDep() 方法,它内部最先调用 dep.addSub(this),把watcher 对象,添加到 dep.subs.push(watcher) 中,也就是把订阅者添加到 depsubs 数组中,当数据变化的时候调用 watcher 对象的update() 方法
  3. 什么时候设置的 Dep.target? 通过首次渲染的案例调试观察。调用 mountComponent() 方法的时候,创建了渲染 watcher 对象,执行 watcher 中的get() 方法
  4. get() 方法内部调用 pushTarget(this),把当前 Dep.target = watcher,同时把当前 watcher 入栈, 因为有父子组件嵌套的时候先把父组件对的 watcher 入栈,再去处理子组件的 watcher,子 组件的处理完毕 后,再把父组件对应的 watcher 出栈,继续操作
  5. Dep.target 用来存放目前正在使用的 watcher。全局唯一,并且一次也只能有一个 watcher 被使用
Watcher 类

Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher

渲染 Watcher 的创建时机

src/core/instance/lifecycle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = () => {
// vm._render() 创建VNode (_render()是用户传入的或者是模板编译后生成的)
// vm._update() 对比两个VNode的差异,将VNode转换为DOM
vm._update(vm._render(), hydrating)
}._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 我们在watcher的构造函数中设置为vm._watcher,
// 因为watcher的初始补丁可能会调用$forceUpdate(例如在子组件的挂载钩子中),
// 这依赖于vm._watcher已经被定义
new Watcher(vm, updateComponent, noop, {
// 触发声明周期钩子 beforeUpdate
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
// 表示当前创建的是渲染watcher
true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
  • 渲染 wacher 创建的位置 lifecycle.jsmountComponent 函数中
  • Wacher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
  • 调用 this.get() ,它里面调用 pushTarget()然后 this.getter.call(vm, vm) (对于渲染 wacher 调用 updateComponent),如果是用户 wacher 会获取属性的值(触发get操作)
  • 当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 wacherupdate() 方法
  • update() 中调用 queueWatcher()
  • queueWatcher() 是一个核心方法,去除重复操作,调用 flushSchedulerQueue() 刷新队列并执行watcher
  • flushSchedulerQueue() 中对 wacher 排序,遍历所有 wacher ,如果有 before,触发生命周期的钩子函数 beforeUpdate,执行 wacher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染 wachercbnoop)

  • 整个流程结束

响应式处理过程总结

数据响应式原理

  • 使用 new Vue() 创建 Vue 实例时,触发 src\core\instance\index.js 中的 Vue 构造函数,从而调用_init() 方法,_init() 方法是在 initMixin()中进行注册的;在 src/core/instance/init.js 中导出 initMixin() ,并在 initMixin() 中注册 _init() 方法,_init() 是整个 Vue 的入口;在 _init() 中调用 initState() 初始化 vm 的 _props/methods/_data/computed/watch,在initState方法中调用了initData()initData()是把 data 中的成员 注入到 Vue 实例中,并且调用 observe(data)data 对象转化成响应式的对象。

  • src/core/observer/index.js 中定义 observe()observe() 是数据响应式的入口,

    • 判断 value 是否是对象 或者 value 是否是 VNode 的实例,如果不是对象,但是是 VNode 则直接返回;

    • 判断 value 对象是否有 __ob__

      • 如果有直接返回 observer 对象,类似于缓存,提升性能
      • 如果没有,则创建 observe 对象,返回 observer 对象。
  • 创建 observer 对象,即 new 一个 Observer 的实例。Observer 构造函数 在 src/core/observer/index.js 中定义,给当前传入的 value 对象(即 vm._data) 添加不可枚举的__ob__属性,并将当前的 observer 实例对象挂载到 value.__ob__ 中,然后再进行数组的响应式处理和对象的响应式处理。

    • 数组的响应式处理,就是重写数组中修改原数组的方法,如 push、pop、shift 等,当执行数组的 push、unshift、splice(插入或替换元素) 方法 ,对数组中新插入的元素,会调用 observer 实例的 observeArray() 方法,重新遍历数组元素,并将其设置为响应式数据。最后,调用数组的 observer 对象中的 dep 依赖的 notify()方法,进行发送通知操作。
    • 对象的响应式处理,就是调用 observer 对象的 walk() 方法,遍历对象中的每一个属性,调用 defineReactive() ,为每一个属性添加 setter / getter
  • defineReactive 方法,为每一个属性创建 dep 实例对象,dep负责为当前属性 key 收集依赖,即收集当前观察属性的 Watcher。如果当前属性的值是对象,会进行深度监听,并调用observedefineReactive中利用 Object.defineProperty()为属性添加 gettersetter。其中,getter 的作用是收集依赖,即为当前的Watcher对象添加依赖,1个 watcher 会对应多个 dep(即,要观察的属性很多) 。如果这个属性的值是对象,那也要给子对象添加依赖,最后返回属性的值。在 setter 中,先保存新值,如果新值是对象,也要调用 observe ,观察子对象并返回子对象的 observer 对象,然后,调用dep.notify(),进行派发更新(发送通知)。

  • 收集依赖时,在watcher对象的get方法中调用pushTarget,记录Dep.target属性。访问data中的成员的时候收集依赖,defineReactivegetter中收集依赖。把属性对应的 watcher 对象添加到depsubs数组中,给childOb收集依赖,目的是子对象添加和删除成员时发送通知。

  • 在数据发生变化的时候,会调用dep.notify()发送通知,在dep.notify()中会调用watcher对象的update()方法,update()中的调用的queueWatcher()去判断watcher是否被处理,如果watcher没有被处理,则添加到queue队列中,并调用flushScheduleQueue()

  • flushScheduleQueue() 中,会渲染 Watcher, 触发 beforeUpdate 生命钩子函数,并调用watcher.run(),run() 中调用 get() ,获取属性的新值,get() 中,使用 value = this.getter.call(vm, vm) 获取新值,调用 this.cb(),即调用 updateComponent,渲染 watcher。最后,调用 resetSchedulerState(),清空上一次的依赖;调用 callActivatedHooks(activatedQueue),触发 actived 钩子函数;调用 callUpdatedHooks(updatedQueue),触发 updated 钩子函数。

之前提出的问题现在答案就很明确了

  • vm.msg = { count: 0 } 重新给属性赋值,是否是响应式的?
    是响应式的
  • vm.arr.push(4) 视图是否会更新?
    视图是会更新
  • vm.arr[0] = 4 给数组元素赋值,视图是否会更新?
    视图不会更新,数组赋值可以使用vm.arr.splice(0,1,4)
  • vm.arr.length = 0 修改数组的 length,视图是否会更新?
    视图不会更新,清空数组可以使用vm.arr.splice(0)

处理数组响应式的时候并没有遍历数组中的所有属性,而是遍历所有元素把是对象的元素转换为响应式对象,并没有处理数组对象的属性,数组中元素会很多,处理的会导致性能问题

实例方法/数据

vm.$set

功能

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')

1
vm.$set(obj, 'foo', 'test')

注意:对象不能是 Vue 实例,或者 Vue 实例的根数据对象($data)。

源码

定义位置

1
2
3
4
// 静态方法 set/delete/nextTick 
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
1
2
3
4
5
6
7
// 注册 vm 的 $data/$props/$set/$delete/$watch 
// instance/index.js
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del

set() 方法
src/core/observer/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
// 设置对象的属性。添加新的属性,如果该属性不存在,则触发更改通知
export function set (target: Array<any> | Object, key: any, val: any): any {
// 判断target是否是undefined或者是原始值,因为不能为undefined和原始值增加属性
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
/* 数组处理 */
// 判断 target 是否是数组,key是否是合法的索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 判断当前key和数组length的最大值给length
// 当我们调用$set传递的索引有可能超过数组的length属性
target.length = Math.max(target.length, key)
// 通过 splice 对 key 位置的元素进行替换
// splice在array.js进行了响应化的处理
target.splice(key, 1, val)
return val
}
/* 对象处理 */
// 如果 key 在对象中已经存在且不是原型成员 直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取 target 中的 observer 对象
// 响应式处理中会给每个对象增加一个__ob__属性,__ob__存储的是observer对象
const ob = (target: any).__ob__
// 如果 target 是 vue 实例或者 $data 直接返回($data的ob.vmCount值为1,其他对象为0 )
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果 ob 不存在,target 不是响应式对象直接赋值
if (!ob) {
target[key] = val
return val
}
// 如果ob对象存在,调用 defineReactive 把 key 设置为响应式属性
defineReactive(ob.value, key, val)
// 发送通知 (这里可以这样调用原因是,在收集依赖的时候为每一个子对象创建了childob,并给childob的ob也收集了依赖,所以这里可以直接发送通知)
ob.dep.notify()
return val
}

vm.$delete

功能

删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue不能检测到属性被删除的限制,但是你应该很少会使用它。

1
vm.$delete(vm.obj, 'msg') 1

注意:目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。

源码

定义位置

  • Vue.delete()

src/core/global-api/index.js

1
2
3
4
// 静态方法 set/delete/nextTick 
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
  • vm.$delete()

src/core/instance/index.js

1
2
3
4
5
6
// 注册 vm 的 $data/$props/$set/$delete/$watch 
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del

del()方法

src/core/observer/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Delete a property and trigger change if necessary.
*/
// 删除一个属性并在必要时触发更改
export function del (target: Array<any> | Object, key: any) {
// 判断target是否是undefined或者是原始值,因为不能为undefined和原始值增加属性
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
//数组的处理: 判断是否是数组,以及key是否合法
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果是数组通过splice删除
// splice做过响应式处理
target.splice(key, 1)
return
}
//对象的处理: 获取target的ob对象
const ob = (target: any).__ob__
// target 如果是 vue实例或者$data对象,直接返回
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果 target 对象没有 key 属性或者是继承来的key直接返回
if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
// 是否是响应式对象(是否存在ob对象)
if (!ob) {
return
}
// 通过 ob 发送通知
ob.dep.notify()
}

vm.$watch

观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

1
vm.$watch( expOrFn, callback, [options] )
  • expOrFn:要监视的 $data 中的属性,可以是表达式或函数
  • callback:数据变化后执行的函数
    • 函数:回调函数
    • 对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应的定义
  • options:可选的选项
    • deep:布尔类型,深度监听
    • immediate:布尔类型,是否立即执行一次回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const vm = new Vue({ 
el: '#app',
data: {
a: '1',
b: '2',
msg: 'Hello Vue',
user: {
firstName: '诸葛',
lastName: '亮'
}
}
})

// expOrFn 是表达式
vm.$watch('msg', function (newVal, oldVal) {
console.log(newVal, oldVal)
})
vm.$watch('user.firstName', function (newVal, oldVal) {
console.log(newVal)
})
// expOrFn 是函数
vm.$watch(function () {
return this.a + this.b
}, function (newVal, oldVal) {
console.log(newVal)
})
// deep 是 true,消耗性能
// 设置deep深度监听user中属性,不设置的话只会监听user变化,里面属性变化后不会更新视图
vm.$watch('user', function (newVal, oldVal) {
// 此时的 newVal 是 user 对象
console.log(newVal === vm.user)
}, {
deep: true
})
// immediate 是 true
// 正常情况下user发生变化时候才会执行回调函数,想要立即执行,设置第三个参数immediate
vm.$watch('msg', function (newVal, oldVal) {
console.log(newVal)
}, {
immediate: true
})

三种类型的 Watcher 对象

  • 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 创建顺序:计算属性 Watcher(id:1)、用户 Watcher (侦听器)(id:2)、渲染 Watcher(id:3)
  • 执行顺序:按照id从小到大顺序,与创建顺序相同
  • vm.$watch()

src/core/instance/state.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 //$watch 监视数据变化
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 获取 vue 实例 this
const vm: Component = this
if (isPlainObject(cb)) {
// 判断如果 cb 是对象执行 createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记为用户 watcher
options.user = true
// 创建用户 watcher 对象
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判断 immediate 如果为 true
if (options.immediate) {
// 立即执行一次 cb 回调,并且把当前值传入
//try...catch作用: 不确定回电函数是否安全,确保回电函数执行错误不会影响代码执行
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消监听的方法
return function unwatchFn () {
watcher.teardown()
}
}

异步更新队列 -nextTick()

Vue 更新 DOM 是异步执行的,批量的

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

1
vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})

定义位置
src/core/instance/render.js

1
2
3
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
// 表示已经处理结束
pending = false
// 备份callbacks数组
const copies = callbacks.slice(0)
// 将callbacks内容清空
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// IOS来发中使用浏览器控件不完全支持Promise,在iOS >= 9.3.3时候不会使用Promise,会使用setTimeou
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
//timerFunc中优先使用Promise(微任务)处理flushCallbacks
// 微任务,在本次同步任务执行完毕以后,执行微任务
/**
* nextTick方法的作用:获取DOM上最新的数据,那么当微任务执行的时候DOM元素还没有渲染到浏览器上,此时nextTick如何获取值?
* 当nextTick中的回调函数执行之前,数据已经被改变了,当我们重新改变数据的时候会立即发送通知,通知watcher渲染视图但是在
* watcher内部首先做的是把DOM中的数据进行更新也就是更改DOM树,当前事件循环结束之后才会在浏览器中执行DOM的更新操作,nextTick中如果
* 使用promise(微任务)的话,获取数据的时候是从DOM树上直接获取的,此时DOM还没有渲染到浏览器上
*/
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
// 处理IOS兼容问题
if (isIOS) setTimeout(noop)
}
// 标记使用微任务
isUsingMicroTask = true
// MutationObserver 监听 DOM 对象的改变,如果DOM改变后会执行一个回调函数(以微任务形式执行)
// MutationObserver在IE10,IE11才支持,在IE11中也是不完全支持
// 当前浏览器不是IE浏览器并且支持MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4 兼容 PhantomJS, iOS7, Android 4.4浏览器
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
// 标记为使用微任务
isUsingMicroTask = true
// setImmediate(时间设置为0时候,是立即支持)只有IE和nodejs支持,类似于setTimeout(时间设置为0时候,最快等待4ms才执行),性能比setTimeout好
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

/**
* nextTick在执行回调函数的时候,会先将回调函数放在callbacks数组中,优先以微任务方式处理回调函数,如果浏览器不支持微任务的话会将降级成宏任务
*/
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把 cb 加上异常处理存入callbacks数组
callbacks.push(() => {
// cb 用户传入的
if (cb) {
try {
// 调用cb
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 用来接收promise传入的resolve
_resolve(ctx)
}
})
// 队列是否正在被处理
if (!pending) {
// 正在处理中
pending = true
// nextTick核心timerFunc处理
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
// 返回promise对象
return new Promise(resolve => {
_resolve = resolve
})
}
}

参考

vue响应式原理
深入剖析Vue源码
深入响应式原理
Array.sort内部实现原理